// -------------------------------------------------
// BlinKit - BlinKit Library
// -------------------------------------------------
//   File Name: script_controller_impl.cpp
// Description: ScriptController Class
//      Author: Ziming Li
//     Created: 2022-03-02
// -------------------------------------------------
// Copyright (C) 2022 MingYang Software Technology.
// -------------------------------------------------

#include "../script_controller.h"

#include "blinkit/blink/renderer/bindings/core/qjs/context_client.h"
#include "blinkit/blink/renderer/bindings/core/qjs/qjs_bindings.h"
#include "blinkit/blink/renderer/bindings/core/script_source_code.h"
#include "blinkit/blink/renderer/bindings/core/script_wrappable.h"
#include "blinkit/blink/renderer/core/dom/element.h"
#include "blinkit/blink/renderer/core/dom/microtask.h"
#include "blinkit/blink/renderer/core/events/Event.h"
#include "blinkit/blink/renderer/core/loader/frame_loader_client.h"
#include "blinkit/blink/renderer/core/frame/local_frame.h"
#include "blinkit/js/runtime_impl.h"
#ifndef NDEBUG
#   include "blinkit/blink/renderer/core/EventNames.h"
#   include "blinkit/blink/renderer/bindings/core/qjs/qjs.h"
#endif

namespace blink {

ScriptController::Prototypes::Prototypes(void)
{
    for (size_t i = 0; i < std::size(prototypes); ++i)
        prototypes[i] = JS_UNINITIALIZED;
}

void ScriptController::Prototypes::Cleanup(JSContext *ctx)
{
    CleanupPrototypeMap(ctx, eventPrototypes);
    CleanupPrototypeMap(ctx, elementPrototypes);
    for (size_t i = 0; i < std::size(prototypes); ++i)
    {
        JS_FreeValue(ctx, prototypes[i]);
        prototypes[i] = JS_UNINITIALIZED;
    }
}

void ScriptController::Prototypes::CleanupPrototypeMap(JSContext *ctx, PrototypeMap &protoMap)
{
    for (const auto &[_, proto] : protoMap)
        JS_FreeValue(ctx, proto);
    protoMap.clear();
}

JSValue ScriptController::Prototypes::FindElementPrototype(const AtomicString &tagName) const
{
    auto it = elementPrototypes.find(tagName);
    return elementPrototypes.end() != it ? it->second : prototypes[qjs::PROTO_Element];
}

JSValue ScriptController::Prototypes::FindEventPrototype(const AtomicString &interfaceName) const
{
    auto it = eventPrototypes.find(interfaceName);
    if (eventPrototypes.end() != it)
        return it->second;

#ifndef NDEBUG
    if (EventNames::Event != interfaceName)
        ZLOG("WARNING: Event interface '{}' not found.", interfaceName.stdUTF8());
#endif
    return prototypes[qjs::PROTO_Event];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

ScriptController::ScriptController(LocalFrame &frame) : m_frame(frame)
{
}

ScriptController::~ScriptController(void)
{
    ASSERT(nullptr == m_ctx);
    ASSERT(!m_rt);
}

void ScriptController::Clear(void)
{
    if (!m_rt)
        return;

    ASSERT(nullptr != m_ctx);

    if (nullptr != m_client)
        m_client->ContextDestroy(m_ctx);

    m_rt->RunGC();
    ASSERT(m_eventListeners.empty());

    m_prototypes.Cleanup(m_ctx);

    JS_FreeContext(m_ctx);
    m_ctx = nullptr;

    m_rt.reset();
}

void ScriptController::clearForClose(void)
{
    Clear();
}

void ScriptController::clearWindowProxy(void)
{
    Clear();
}

JSContext* ScriptController::EnsureContext(void)
{
    if (!m_rt)
    {
        m_rt = std::make_unique<RuntimeImpl>(this);

        ASSERT(nullptr == m_ctx);
        m_ctx = m_rt->CreateContext();

        RegisterCommonPrototypes(m_ctx, m_prototypes);
        RegisterPrototypes(m_ctx, m_prototypes);

        PrepareContext(m_ctx, m_prototypes);

        if (nullptr != m_client)
            m_client->ContextReady(m_ctx);
    }
    return m_ctx;
}

void ScriptController::ExecutePendingJobs(void)
{
    if (nullptr != m_ctx)
        Microtask::PerformCheckpoint(JS_GetRuntime(m_ctx));
}

void ScriptController::executeScriptInMainWorld(const ScriptSourceCode &sourceCode)
{
    EnsureContext();

    std::string input = sourceCode.source().stdUtf8();
    JSValue ret = JS_Eval(m_ctx, input.c_str(), input.length(), sourceCode.FileName().c_str(), JS_EVAL_TYPE_GLOBAL);
#ifndef NDEBUG
    if (JS_IsException(ret))
    {
        qjs::DumpException(m_ctx);
        ASSERT(!JS_IsException(ret));
    }
#endif
    JS_FreeValue(m_ctx, ret);
}

EventListener* ScriptController::FindEventListener(JSValue scriptObject) const
{
    auto it = m_eventListeners.find(JS_VALUE_GET_PTR(scriptObject));
    return m_eventListeners.end() != it ? it->second : nullptr;
}

ScriptController* ScriptController::From(JSContext *ctx)
{
    JSRuntime *rt = JS_GetRuntime(ctx);

    ScriptController *ret = reinterpret_cast<ScriptController *>(JS_GetRuntimeOpaque(rt));
    ASSERT(nullptr == ret || ret->m_ctx == ctx);
    return ret;
}

LocalDOMWindow* ScriptController::GetWindow(void)
{
    return m_frame.localDOMWindow();
}

JSValue ScriptController::NewElement(Element *element)
{
    JSValue proto = m_prototypes.FindElementPrototype(element->localName());
    return NewScriptWrappable(element, proto);
}

JSValue ScriptController::NewNode(Node *node)
{
    using namespace qjs;

    if (node->isElementNode())
        return NewElement(toElement(node));

    if (node->isTextNode())
        return NewScriptWrappable(node, m_prototypes.prototypes[PROTO_Text]);

    if (node->isDocumentNode())
        return NewScriptWrappable(node, m_prototypes.prototypes[PROTO_Document]);

    ASSERT(false); // BKTODO:
    return JS_NULL;
}

JSValue ScriptController::NewScriptWrappable(ScriptWrappable *nativeObject, JSValue proto)
{
    ASSERT(JS_IsObject(proto));
    ASSERT(nullptr == nativeObject->m_ptr);
    return nativeObject->Wrap(m_ctx, proto);
}

void ScriptController::PrepareContext(JSContext *ctx, const Prototypes &prototypes)
{
    using namespace qjs;

    JSValue global = JS_GetGlobalObject(ctx);

    JSValue window = prototypes.prototypes[PROTO_Window];
    ASSERT(JS_IsObject(window));
    JS_SetPrototype(ctx, global, window);

    JS_FreeValue(ctx, global);
}

void ScriptController::RegisterCommonPrototypes(JSContext *ctx, Prototypes &dst)
{
    using namespace qjs;
    dst.prototypes[PROTO_DOMTokenList] = CreateDOMTokenListPrototype(ctx);
    dst.prototypes[PROTO_Event] = CreateEventPrototype(ctx);
    dst.prototypes[PROTO_HTMLCollection] = CreateHTMLCollectionPrototype(ctx);
    dst.prototypes[PROTO_NodeList] = CreateNodeListPrototype(ctx);
}

void ScriptController::RegisterCharacterDataPrototypes(JSContext *ctx, Prototypes &dst, JSValue protoNode)
{
    using namespace qjs;

    JSValue protoCharacterData = CreateCharacterDataPrototype(ctx, protoNode);
    dst.prototypes[PROTO_Text] = CreateTextPrototype(ctx, protoCharacterData);
    dst.prototypes[PROTO_Comment] = CreateCommentPrototype(ctx, protoCharacterData);
    JS_FreeValue(ctx, protoCharacterData);
}

void ScriptController::RegisterEventListener(JSValue scriptObject, EventListener *listener)
{
    void *key = JS_VALUE_GET_PTR(scriptObject);
    ASSERT(!zed::key_exists(m_eventListeners, key));
    m_eventListeners.emplace(key, listener);
}

void ScriptController::UnregisterEventListener(JSValue scriptObject)
{
    void *key = JS_VALUE_GET_PTR(scriptObject);
    ASSERT(zed::key_exists(m_eventListeners, key));
    m_eventListeners.erase(key);
}

void ScriptController::updateDocument(void)
{
    Clear();
}

JSValue ScriptController::WrapElement(Element *element)
{
    if (nullptr == element)
        return JS_NULL;
    if (element->IsBound())
        return element->DupValue(m_ctx);
    return NewElement(element);
}

JSValue ScriptController::WrapEvent(Event *event)
{
    using namespace qjs;

    if (nullptr == event)
        return JS_NULL;

    if (event->IsBound())
        return event->DupValue(m_ctx);

    ASSERT(!event->isTouchEvent()); // BKTODO:
    ASSERT(!event->isGestureEvent()); // BKTODO:
    ASSERT(!event->isWheelEvent()); // BKTODO:
    ASSERT(!event->isRelatedEvent()); // BKTODO:
    ASSERT(!event->isPointerEvent()); // BKTODO:
    ASSERT(!event->isDragEvent()); // BKTODO:
    ASSERT(!event->isClipboardEvent()); // BKTODO:
    ASSERT(!event->isBeforeTextInsertedEvent()); // BKTODO:
    ASSERT(!event->isBeforeUnloadEvent()); // BKTODO:

    JSValue proto = m_prototypes.FindEventPrototype(event->interfaceName());
    return NewScriptWrappable(event, proto);
}

JSValue ScriptController::WrapEventTarget(EventTarget *eventTarget)
{
    if (nullptr == eventTarget)
        return JS_NULL;

    if (eventTarget->IsBound())
        return eventTarget->DupValue(m_ctx);

    if (Node *node = eventTarget->toNode())
        return NewNode(node);

    if (LocalDOMWindow *window = eventTarget->toDOMWindow())
        return JS_GetGlobalObject(m_ctx);

    ASSERT_NOT_REACHED();
    return JS_NULL;
}

JSValue ScriptController::WrapImpl(ScriptWrappable *nativeObject, int prototype)
{
    if (nullptr == nativeObject)
        return JS_NULL;

    if (nativeObject->IsBound())
        return nativeObject->DupValue(m_ctx);
    return NewScriptWrappable(nativeObject, m_prototypes.prototypes[prototype]);
}

JSValue ScriptController::WrapNode(Node *node)
{
    if (nullptr == node)
        return JS_NULL;

    if (node->IsBound())
        return node->DupValue(m_ctx);

    return NewNode(node);
}

}
